<link rel="import" href="observe-js-import.html">

<script>

(function() {
  ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(function(m) {
    var orig = Polymer.Base[m];
    Polymer.Base[m] = function(path) {
      var obj = this.get(path);
      this._flushObserveJsObservers(obj);
      orig.apply(this, arguments);
      this._discardObserveJsObservers(obj);
    };
  }, this);
})();

Polymer.ObserveJsBehavior = {

  _observeJsStatic: {
    count: 0,
    observerMap: new WeakMap()
  },

  created: function() {
    this._observeJsId = this._observeJsStatic.count++;
  },

  _propertyChanged: function(path, newValue, oldValue) {
    if (oldValue && typeof oldValue == 'object') {
      this._removeObserveJsObserver(oldValue, path);
    }
    if (newValue && typeof newValue == 'object') {
      this._addObserveJsObserver(newValue, path);
    }
  },

  // Bookkeeping here ensures only one observer is created for each object
  // throughout the tree, with a list of elements and paths that observed the
  // array; the same changes are notified to each element/path using the same
  // change record (in the case of array notification) to ensure base
  // dirty-checking filters out changes already notified through one part of
  // the tree
  _addObserveJsObserver: function(object, path) {
    var obs = this._observeJsStatic.observerMap.get(object);
    if (!obs) {
      var els = {};
      obs = {
        count: 1,
        observer: this._createObserveJsObserver(object, els),
        elements: els
      };
      this._observeJsStatic.observerMap.set(object, obs);
    } else {
      obs.count++;
    }
    var el = obs.elements[this._observeJsId];
    if (!el) {
      el = obs.elements[this._observeJsId] = {
        element: this,
        paths: {},
        count: 1
      };
      el.paths[path] = 1;
    } else {
      var count = el.paths[path];
      if (!count) {
        el.count++;
      }
      el.paths[path] = count++;
    }
  },

  _createObserveJsObserver: function(object, els) {
    var observer;
    if (Array.isArray(object)) {
      observer = new ArrayObserver(object);
      observer.open(function(splices) {
        this._notifyObserveJsPath(els, 'splices', {
          keySplices: Polymer.Collection.applySplices(object, splices),
          indexSplices: splices
        });
      }, this);
    } else {
      observer = new ObjectObserver(object);
      observer.open(function(added, removed, changed) {
        Object.keys(added).forEach(function(prop) {
          this._notifyObserveJsPath(els, prop, added[prop]);
        }, this);
        Object.keys(removed).forEach(function(prop) {
          this._notifyObserveJsPath(els, prop, undefined);
        }, this);
        Object.keys(changed).forEach(function(prop) {
          this._notifyObserveJsPath(els, prop, changed[prop]);
        }, this);
      }, this);
    }
    return observer;
  },

  _notifyObserveJsPath: function(els, prop, value) {
    for (var id in els) {
      var el = els[id];
      for (var path in el.paths) {
        el.element.set(path + '.' + prop, value);
      }
    }
  },

  _removeObserveJsObserver: function(object, path) {
    var obs = this._observeJsStatic.observerMap.get(object);
    var el = obs.elements[this._observeJsId];
    if (--el.paths[path] === 0) {
      delete el.paths[path];
      if (--el.count === 0) {
        delete obs.elements[this._observeJsId];
        if (--obs.count === 0) {
          obs.observer.close();
          obs.observer = null;
        }
      }
    }
  },

  _flushObserveJsObservers: function(object) {
    var obs = this._observeJsStatic.observerMap.get(object);
    if (obs) {
      obs.observer.deliver();
    }
  },

  _discardObserveJsObservers: function(object) {
    var obs = this._observeJsStatic.observerMap.get(object);
    if (obs) {
      obs.observer.discardChanges();
    }
  },

  // Hook user code entry points to poll observe-js

  _listen: function(node, eventName, handler) {
    var host = this;
    Polymer.Base._listen.call(this, node, eventName, function() {
      handler.apply(host, arguments);
      host._observerCheckpoint();
    });
  },

  async: function(callback, waitTime) {
    var host = this;
    Polymer.Base.async.call(this, function() {
      callback.apply(host, arguments);
      host._observerCheckpoint();
    }, waitTime);
  },

  debounce: function(jobName, callback, wait) {
    Polymer.Base.debounce.call(this, jobName, callback, function() {
      callback.apply(this, arguments);
      this._observerCheckpoint();
    }, wait);
  },

  _observerCheckpoint: function() {
    Polymer.ObserveJsBehavior.debouncer =
      Polymer.Debounce(Polymer.ObserveJsBehavior.debouncer,
        Platform.performMicrotaskCheckpoint);
  }

};


// Poll observe-js all the time
setInterval(Platform.performMicrotaskCheckpoint, 125);

</script>
